﻿using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using Telerik.JustDecompiler.Ast;
using Telerik.JustDecompiler.Ast.Expressions;
using Telerik.JustDecompiler.Ast.Statements;

namespace Telerik.JustDecompiler.Steps.CodePatterns
{
    static class CatchClausesFilterPattern
    {
        /*
         * The following pattern matches BlockStatements with similar content
         * {
         *      stackVariable6 = exception_0 as Exception;
         *      if (stackVariable6 != null)
	     *      {
         *          ProjectData.SetProjectError(stackVariable6);
         *          ex = stackVariable6;
		 *          stackVariable15 = Program.Bool() && b is int;
	     *      }
	     *      else
	     *      {
		 *          stackVariable15 = false;
	     *      }
	     *      stackVariable15;
         * }
         * - stackVariable6 = exception_0 as Exception; - the VariableDeclarationExpression is build using this variable reference
         * - Program.Bool() && b is int - The filter expression
         * - ex = stackVariable6; - Not required line, if present the variable reference is used to build the VariableDeclarationExpression
         * - ProjectData.SetProjectError(stackVariable6); - Not required line. It is generated by the VB compiler. It could be after ex = stackVariable6;
        */
        public static bool TryMatch(BlockStatement filter, out VariableDeclarationExpression variableDeclaration, out Expression filterExpression)
        {
            variableDeclaration = null;
            filterExpression = null;

            if (!TryMatchVariableDeclaration(filter, out variableDeclaration))
            {
                return false;
            }

            // Save cast of the exception
            // If-else statement for the filter expression
            // Variable reference of the result of the filter block
            if (filter.Statements.Count != 3)
            {
                return false;
            }

            IfStatement ifStatement = filter.Statements[1] as IfStatement;
            if (ifStatement == null)
            {
                return false;
            }

            BlockStatement exceptionAssignmentBlock = null;
            BlockStatement negativeFilterResultAssignmentBlock = null;
            if ((ifStatement.Condition as BinaryExpression).Operator == BinaryOperator.ValueInequality)
            {
                exceptionAssignmentBlock = ifStatement.Then;
                negativeFilterResultAssignmentBlock = ifStatement.Else;
            }
            else
            {
                exceptionAssignmentBlock = ifStatement.Else;
                negativeFilterResultAssignmentBlock = ifStatement.Then;
            }

            ExpressionStatement exceptionAssignmentStatement = null;
            ExpressionStatement methodInvocationStatement = null;
            if ((exceptionAssignmentBlock.Statements.Count != 1 && exceptionAssignmentBlock.Statements.Count != 2 && exceptionAssignmentBlock.Statements.Count != 3) ||
                negativeFilterResultAssignmentBlock.Statements.Count != 1)
            {
                return false;
            }

            if (exceptionAssignmentBlock.Statements.Count == 2)
            {
                ExpressionStatement firstThenStatement = exceptionAssignmentBlock.Statements[0] as ExpressionStatement;
                if (firstThenStatement == null)
                {
                    return false;
                }

                if (firstThenStatement.Expression.CodeNodeType == CodeNodeType.BinaryExpression)
                {
                    exceptionAssignmentStatement = firstThenStatement;
                }
                else if (firstThenStatement.Expression.CodeNodeType == CodeNodeType.MethodInvocationExpression)
                {
                    methodInvocationStatement = firstThenStatement;
                }
                else
                {
                    return false;
                }
            }
            else if (exceptionAssignmentBlock.Statements.Count == 3)
            {
                ExpressionStatement firstThenStatement = exceptionAssignmentBlock.Statements[0] as ExpressionStatement;
                ExpressionStatement secondThenStatement = exceptionAssignmentBlock.Statements[1] as ExpressionStatement;
                if (firstThenStatement == null || secondThenStatement == null)
                {
                    return false;
                }

                if (firstThenStatement.Expression.CodeNodeType == CodeNodeType.BinaryExpression &&
                    secondThenStatement.Expression.CodeNodeType == CodeNodeType.MethodInvocationExpression)
                {
                    exceptionAssignmentStatement = firstThenStatement;
                    methodInvocationStatement = secondThenStatement;
                }
                else if (firstThenStatement.Expression.CodeNodeType == CodeNodeType.MethodInvocationExpression &&
                    secondThenStatement.Expression.CodeNodeType == CodeNodeType.BinaryExpression)
                {
                    methodInvocationStatement = firstThenStatement;
                    exceptionAssignmentStatement = secondThenStatement;
                }
                else
                {
                    return false;
                }
            }

            if (exceptionAssignmentStatement != null)
            {
                BinaryExpression exceptionAssignment = exceptionAssignmentStatement.Expression as BinaryExpression;
                if (exceptionAssignment == null || !exceptionAssignment.IsAssignmentExpression ||
                    exceptionAssignment.ExpressionType.FullName != variableDeclaration.ExpressionType.FullName)
                {
                    return false;
                }

                VariableReferenceExpression left = exceptionAssignment.Left as VariableReferenceExpression;
                VariableReferenceExpression right = exceptionAssignment.Right as VariableReferenceExpression;
                if (left == null || right == null)
                {
                    return false;
                }
            }

            if (methodInvocationStatement != null)
            {
                MethodInvocationExpression methodInvocation = methodInvocationStatement.Expression as MethodInvocationExpression;
                if (methodInvocation == null ||
                    methodInvocation.MethodExpression.Method.FullName != "System.Void Microsoft.VisualBasic.CompilerServices.ProjectData::SetProjectError(System.Exception)")
                {
                    return false;
                }
            }
            
            ExpressionStatement lastExpressionStatement = filter.Statements[2] as ExpressionStatement;
            if (lastExpressionStatement == null)
            {
                return false;
            }

            VariableReferenceExpression lastExpression = lastExpressionStatement.Expression as VariableReferenceExpression;
            if (lastExpression == null)
            {
                return false;
            }

            if (!TryMatchFilterExpression(ifStatement, variableDeclaration.Variable.VariableType, lastExpression, out filterExpression))
            {
                return false;
            }

            return true;
        }

        private static bool TryMatchVariableDeclaration(BlockStatement filter, out VariableDeclarationExpression variableDeclaration)
        {
            variableDeclaration = null;

            ExpressionStatement firstExpressionStatement = filter.Statements[0] as ExpressionStatement;
            if (firstExpressionStatement == null)
            {
                return false;
            }

            BinaryExpression binaryExpression = firstExpressionStatement.Expression as BinaryExpression;
            if (binaryExpression == null || !binaryExpression.IsAssignmentExpression)
            {
                return false;
            }

            VariableReferenceExpression variableReference = binaryExpression.Left as VariableReferenceExpression;
            if (variableReference == null)
            {
                return false;
            }

            SafeCastExpression safeCast = binaryExpression.Right as SafeCastExpression;
            if (safeCast == null)
            {
                return false;
            }

            IfStatement ifStatement = filter.Statements[1] as IfStatement;
            if (ifStatement == null)
            {
                return false;
            }

            BinaryExpression condition = ifStatement.Condition as BinaryExpression;
            if (condition == null)
            {
                return false;
            }

            VariableReferenceExpression conditionLeft = condition.Left as VariableReferenceExpression;
            if (conditionLeft == null)
            {
                return false;
            }

            LiteralExpression conditionRight = condition.Right as LiteralExpression;
            if (conditionRight == null)
            {
                return false;
            }

            if (!conditionLeft.Equals(variableReference) || conditionRight.Value != null ||
                (condition.Operator != BinaryOperator.ValueEquality && condition.Operator != BinaryOperator.ValueInequality))
            {
                return false;
            }

            // At this point we know the type of the exception
            VariableDefinition variableDefinition = variableReference.Variable.Resolve();
            IEnumerable<Instruction> instructions = variableReference.MappedInstructions;

            BlockStatement exceptionVariableAssignmentBlock;
            if (condition.Operator == BinaryOperator.ValueInequality)
            {
                exceptionVariableAssignmentBlock = ifStatement.Then;
            }
            else // BinaryOperator.ValueEquality
            {
                exceptionVariableAssignmentBlock = ifStatement.Else;
            }

            ExpressionStatement expressionStatement = exceptionVariableAssignmentBlock.Statements[0] as ExpressionStatement;
            if (!TryGetVariableDeclaration(expressionStatement, variableReference, ref variableDefinition, ref instructions))
            {
                if (exceptionVariableAssignmentBlock.Statements.Count >= 2)
                {
                    expressionStatement = exceptionVariableAssignmentBlock.Statements[1] as ExpressionStatement;
                    TryGetVariableDeclaration(expressionStatement, variableReference, ref variableDefinition, ref instructions);
                }
            }

            variableDeclaration = new VariableDeclarationExpression(variableDefinition, instructions);

            return true;
        }

        private static bool TryGetVariableDeclaration(ExpressionStatement statement, VariableReferenceExpression variableReference, ref VariableDefinition variableDefinition, ref IEnumerable<Instruction> instructions)
        {
            if (statement != null)
            {
                BinaryExpression assignment = statement.Expression as BinaryExpression;
                if (assignment != null && assignment.IsAssignmentExpression)
                {
                    VariableReferenceExpression left = assignment.Left as VariableReferenceExpression;
                    VariableReferenceExpression right = assignment.Right as VariableReferenceExpression;
                    if (left != null && right != null)
                    {
                        if (right.Equals(variableReference))
                        {
                            // At this point we know that in the catch is created variable for the exception
                            variableDefinition = left.Variable.Resolve();
                            instructions = left.MappedInstructions;
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        private static bool TryMatchFilterExpression(IfStatement ifStatement, TypeReference variableType, VariableReferenceExpression lastExpression, out Expression filterExpression)
        {
            filterExpression = null;

            ExpressionStatement resultAssignmentStatement;
            if ((ifStatement.Condition as BinaryExpression).Operator == BinaryOperator.ValueInequality)
            {
                resultAssignmentStatement = ifStatement.Then.Statements[ifStatement.Then.Statements.Count - 1] as ExpressionStatement;
            }
            else
            {
                resultAssignmentStatement = ifStatement.Else.Statements[ifStatement.Then.Statements.Count - 1] as ExpressionStatement;
            }

            if (resultAssignmentStatement == null)
            {
                return false;
            }

            BinaryExpression resultAssignment = resultAssignmentStatement.Expression as BinaryExpression;
            if (!resultAssignment.IsAssignmentExpression)
            {
                return false;
            }

            if (resultAssignment.ExpressionType.FullName != Constants.Boolean)
            {
                return false;
            }

            VariableReferenceExpression resultAssignmentLeft = resultAssignment.Left as VariableReferenceExpression;
            if (resultAssignmentLeft == null)
            {
                return false;
            }

            if (!resultAssignmentLeft.Equals(lastExpression))
            {
                return false;
            }

            filterExpression = resultAssignment.Right;
            return true;
        }

        public static bool TryMatchMethodStructure(BlockStatement blockStatement)
        {
            ExpressionStatement lastStatement = blockStatement.Statements.Last() as ExpressionStatement;
            if (lastStatement == null)
            {
                return false;
            }

            VariableReferenceExpression variableReference = lastStatement.Expression as VariableReferenceExpression;
            if (variableReference == null)
            {
                return false;
            }

            if (variableReference.ExpressionType.FullName != Constants.Boolean)
            {
                return false;
            }

            return true;
        }
    }
}
